<?php
/**
 * Created by 784855684@qq.com.
 * Link: https://github.com/lichtung/wshore
 * User: linzh
 * Date: 7/8/17
 * Time: 9:05 AM
 */
declare(strict_types=1);


namespace application\module\admin\addon;

use application\module\admin\addon\wechat\Enterprise;
use application\module\admin\addon\wechat\entity\Member;
use application\module\admin\addon\wechat\entity\Message;
use application\module\admin\addon\wechat\MessageHookInterface;
use application\module\admin\addon\wechat\model\MemberModel;
use application\module\admin\addon\wechat\model\MessageModel;
use application\module\admin\addon\wechat\model\PaymentModel;
use application\module\admin\addon\wechat\TemplateData;
use application\module\admin\addon\wechat\throws\UnsubscribeException;
use application\module\admin\addon\wechat\throws\WechatException;
use application\module\admin\addon\wechat\WebAuth;
use wshore\core\Cookie;
use wshore\core\Logger;
use wshore\core\Redis;
use wshore\core\Response;
use wshore\Developer;
use SimpleXMLElement;
use wshore\helper\Base64x;
use wshore\helper\Network;
use wshore\throws\db\RecordNotFoundException;
use wshore\throws\NetworkException;

/**
 * Class Wechat 微信开发工具
 * @method Wechat getInstance(...$param) static
 * @package application\module\admin\addon
 */
class Wechat extends Addon
{
    protected function getName(): string
    {
        return 'wechat';
    }

    protected function models(): array
    {
        return [
            MemberModel::class,
            MessageModel::class,
            PaymentModel::class,
        ];
    }

    const MSGTYPE_TEXT = 'text';
    const MSGTYPE_IMAGE = 'image';
    const MSGTYPE_LOCATION = 'location';
    const MSGTYPE_LINK = 'link';
    const MSGTYPE_EVENT = 'event';
    const MSGTYPE_MUSIC = 'music';
    const MSGTYPE_NEWS = 'news';
    const MSGTYPE_VOICE = 'voice';
    const MSGTYPE_VIDEO = 'video';
    const MSGTYPE_SHORTVIDEO = 'shortvideo';
    const EVENT_SUBSCRIBE = 'subscribe';       //订阅
    const EVENT_UNSUBSCRIBE = 'unsubscribe';   //取消订阅
    const EVENT_SCAN = 'SCAN';                 //扫描带参数二维码
    const EVENT_LOCATION = 'LOCATION';         //上报地理位置
    const EVENT_MENU_VIEW = 'VIEW';                     //菜单 - 点击菜单跳转链接
    const EVENT_MENU_CLICK = 'CLICK';                   //菜单 - 点击菜单拉取消息
    const EVENT_MENU_SCAN_PUSH = 'scancode_push';       //菜单 - 扫码推事件(客户端跳URL)
    const EVENT_MENU_SCAN_WAITMSG = 'scancode_waitmsg'; //菜单 - 扫码推事件(客户端不跳URL)
    const EVENT_MENU_PIC_SYS = 'pic_sysphoto';          //菜单 - 弹出系统拍照发图
    const EVENT_MENU_PIC_PHOTO = 'pic_photo_or_album';  //菜单 - 弹出拍照或者相册发图
    const EVENT_MENU_PIC_WEIXIN = 'pic_weixin';         //菜单 - 弹出微信相册发图器
    const EVENT_MENU_LOCATION = 'location_select';      //菜单 - 弹出地理位置选择器
    const EVENT_SEND_MASS = 'MASSSENDJOBFINISH';        //发送结果 - 高级群发完成
    const EVENT_SEND_TEMPLATE = 'TEMPLATESENDJOBFINISH';//发送结果 - 模板消息发送结果
    const EVENT_KF_SEESION_CREATE = 'kfcreatesession';  //多客服 - 接入会话
    const EVENT_KF_SEESION_CLOSE = 'kfclosesession';    //多客服 - 关闭会话
    const EVENT_KF_SEESION_SWITCH = 'kfswitchsession';  //多客服 - 转接会话
    const EVENT_CARD_PASS = 'card_pass_check';          //卡券 - 审核通过
    const EVENT_CARD_NOTPASS = 'card_not_pass_check';   //卡券 - 审核未通过
    const EVENT_CARD_USER_GET = 'user_get_card';        //卡券 - 用户领取卡券
    const EVENT_CARD_USER_DEL = 'user_del_card';        //卡券 - 用户删除卡券
    const EVENT_MERCHANT_ORDER = 'merchant_order';        //微信小店 - 订单付款通知

    const NO_CHECK = 'NO_CHECK'; # 不检查实名验证
    const FORCE_CHECK = 'FORCE_CHECK'; # 检查实名验证
    const OPTION_CHECK = 'OPTION_CHECK'; # 存在即检查

    private $appid = '';
    private $appsecret = '';
    private $token = '';
    private $encodingaeskey = '';

    private $mch_appid = '';
    private $mch_appsecret = '';
    private $mch_id = '';
    private $mch_ip = '';

    private $access_token = '';

    public function __construct()
    {
        parent::__construct();
        $this->appid = $this->config['appid']??'';
        $this->appsecret = $this->config['appsecret']??'';
        $this->token = $this->config['token']??'';
        $this->encodingaeskey = $this->config['encodingaeskey']??'';

        $this->mch_appid = $this->config['mch_appid']??'';
        $this->mch_appsecret = $this->config['mch_appsecret']??'';
        $this->mch_id = $this->config['mch_id']??'';
        $this->mch_ip = $this->config['mch_ip']??'';

    }

    /**
     * @return string
     */
    public function getAppid(): string
    {
        return $this->appid;
    }

    /**
     * @param string $appid
     */
    public function setAppid(string $appid)
    {
        $this->appid = $appid;
    }


    /**
     * 处理推送消息
     * @param MessageHookInterface|null $hook 消息钩子
     * @return void
     */
    public function handlePush(MessageHookInterface $hook = null)
    {
        try {
            Developer::closeTrace();
            $postStr = file_get_contents("php://input") or die('access deny');
            # 创建消息对象
            $message = new Message((array)simplexml_load_string($postStr,
                SimpleXMLElement::class,
                LIBXML_NOCDATA));

            switch ($messageType = $message->type) {
                case Wechat::MSGTYPE_TEXT:
                    $hook and $hook->onText(new MessageModel($message));
                    break;
                case Wechat::MSGTYPE_EVENT:
                    switch ($message->event) {
                        //订阅
                        case Wechat::EVENT_SUBSCRIBE:
                            try {
                                $umodel = new MemberModel($message->from);
                                $umodel->subscribetimes++;
                                $umodel->subscribe = 1;
                                $umodel->save();
                            } catch (RecordNotFoundException $exception) {
                                $info = $this->getSubscribeUserInfo($message->from);
                                $umodel = new MemberModel(new Member($info));
                            }
                            $hook and $hook->onSubscribe($umodel);
                            break;
                        //取消订阅
                        case Wechat::EVENT_UNSUBSCRIBE:
                            try {
                                $umodel = new MemberModel($message->from);
                                $umodel->subscribe = 0;
                                $umodel->unsubscribe_time = WS_NOW;
                                $umodel->save();
                                $hook and $hook->onUnsubscribe($umodel);
                            } catch (RecordNotFoundException $exception) {
                                # 数据库中找不到这个人
                                die('');
                            }
                            break;
                        case Wechat::EVENT_SCAN :
                            break;                 //扫描带参数二维码
                        case Wechat::EVENT_LOCATION:
                            break;          //上报地理位置
                        case Wechat::EVENT_MENU_VIEW :
                            break;                     //菜单 - 点击菜单跳转链接
                        case Wechat::EVENT_MENU_CLICK :
                            break;                  //菜单 - 点击菜单拉取消息
                        case Wechat::EVENT_MENU_SCAN_PUSH :
                            break;       //菜单 - 扫码推事件(客户端跳URL)
                        case Wechat::EVENT_MENU_SCAN_WAITMSG :
                            break; //菜单 - 扫码推事件(客户端不跳URL)
                        case Wechat::EVENT_MENU_PIC_SYS:
                            break;          //菜单 - 弹出系统拍照发图
                        case Wechat::EVENT_MENU_PIC_PHOTO :
                            break;   //菜单 - 弹出拍照或者相册发图
                        case Wechat::EVENT_MENU_PIC_WEIXIN :
                            break;         //菜单 - 弹出微信相册发图器
                        case Wechat::EVENT_MENU_LOCATION :
                            break;      //菜单 - 弹出地理位置选择器
                        case Wechat::EVENT_SEND_MASS :
                            break;         //发送结果 - 高级群发完成
                        case Wechat::EVENT_SEND_TEMPLATE :
                            break; //发送结果 - 模板消息发送结果
                        case Wechat::EVENT_KF_SEESION_CREATE :
                            break;  //多客服 - 接入会话
                        case Wechat::EVENT_KF_SEESION_CLOSE :
                            break;    //多客服 - 关闭会话
                        case Wechat::EVENT_KF_SEESION_SWITCH :
                            break;   //多客服 - 转接会话
                        case Wechat::EVENT_CARD_PASS  :
                            break;         //卡券 - 审核通过
                        case Wechat::EVENT_CARD_NOTPASS :
                            break;    //卡券 - 审核未通过
                        case Wechat::EVENT_CARD_USER_GET:
                            break;        //卡券 - 用户领取卡券
                        case Wechat::EVENT_CARD_USER_DEL :
                            break;         //卡券 - 用户删除卡券
                        case Wechat::EVENT_MERCHANT_ORDER :
                            break;         //微信小店 - 订单付款通知
                        default:
                    }
                    break;
                case
                Wechat::MSGTYPE_IMAGE:
                    $hook and $hook->onImage(new MessageModel($message));
                    break;
                case Wechat::MSGTYPE_LOCATION:
                case Wechat::MSGTYPE_LINK:
                case Wechat::MSGTYPE_MUSIC:
                case Wechat::MSGTYPE_NEWS :
                case Wechat::MSGTYPE_VOICE:
                case Wechat::MSGTYPE_VIDEO:
                case Wechat::MSGTYPE_SHORTVIDEO:
                default:
            }
        } catch (\Throwable $throwable) {
            $debug_message = '<pre>' . print_r(
                    ['wait.' . get_class($throwable),
                        $throwable->getMessage(),
                        $throwable->getTrace(),
                    ],
                    true
                );
            $requesturl = WS_PUBLIC_URL . '/api/sendemail?' . http_build_query([
                    'subject' => '客服消息发生异常',
                    'body' => $debug_message,
                    'secret' => 'ef6efbcf9e48bf1be79a6b323321204d',]);
            Network::getJson($requesturl);
            Logger::getLogger('wechat_module')->fatal($debug_message);
        }
        # 自动回复
        die('');
    }

    /**
     * 设置access_token
     * @param string $access_token
     * @return Wechat
     */
    public function setAccessToken(string $access_token): Wechat
    {
        $this->access_token = $access_token;
        return $this;
    }

    /**
     * 获取access_token
     * @return string
     * @throws WechatException
     */
    public function getAccessToken(): string
    {
        if (!$this->access_token) {
            try {
                $redis = Redis::getInstance();
                $at = $redis->get('wechat.access_token');
                if (!$at) {
                    $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appid}&secret={$this->appsecret}";
                    $res = Network::getJson($url);
                    WechatException::test($res);
                    $this->access_token = $res['access_token'];
                    $redis->set('wechat.access_token', $at, 7000);
                }
            } catch (NetworkException $exception) {
                throw new WechatException('请求网络失败:' . $exception->getMessage());
            }
        }
        return $this->access_token;
    }

//---------------------------- 客服消息 & 模板消息 ---------------------------------------------------------------//

    /**
     * 发送文本客服消息
     * @param string $openid
     * @param string $content
     * @return array
     * @throws WechatException
     */
    public function responseWithText(string $openid, string $content): array
    {
        return $this->responseWith([
            'touser' => $openid,
            'msgtype' => 'text',
            'text' => [
                'content' => $content,
            ],
        ]);
    }

    /**
     * 发送客服消息
     * @param array $data 消息结构{"touser":"OPENID","msgtype":"news","news":{...}}
     * @return array
     * @throws WechatException
     */
    public function responseWith(array $data): array
    {
        $res = Network::postJson('https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=' . $this->getAccessToken(),
            self::jsonEncode($data));
        WechatException::test($res);
        return $res;
    }

    /**
     * 发送模板消息
     * @param string $openid
     * @param TemplateData $data
     * @param string $url
     * @param string $tplid
     * @return array
     * @throws WechatException
     */
    public function sendTemplate(string $openid, TemplateData $data, string $url, string $tplid)
    {
        $json = [
            'touser' => $openid,
            'template_id' => $tplid,
            'url' => $url,
            'data' => $data->toArray(),
        ];
        $res = Network::postJson('https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=' . $this->getAccessToken(), $json);
        WechatException::test($res);
        return $res;
    }

//---------------------------- 通用 ---------------------------------------------------------------//


    /**
     * 获取关注用户信息
     * @param string $openid
     * @return array 用户取消关注时返回0
     * @throws NetworkException
     * @throws UnsubscribeException
     */
    public function getSubscribeUserInfo(string $openid): array
    {
        $url = 'https://api.weixin.qq.com/cgi-bin/user/info?access_token=' . $this->getAccessToken() . '&openid=' . $openid . '&lang=zh_CN';
        $res = Network::getJson($url);
        WechatException::test($res);
        if ($res['subscribe']??0) {
            return $res;
        } else {
            # 用户未关注时返回空数组
            throw new UnsubscribeException($openid);
        }
    }


//---------------------------- WEB授权 ---------------------------------------------------------------//

    /**
     * 获取Web授权URL重定向跳转链接
     * @param string $redirect_uri
     * @param string $state
     * @return string
     */
    public function getWebAuthUrl(string $redirect_uri, string $state = '123'): string
    {
        return 'https://open.weixin.qq.com/connect/oauth2/authorize?' . http_build_query([
                'appid' => $this->appid,
                'redirect_uri' => $redirect_uri,# http_build_query 自带urlencode
                'response_type' => 'code',
                'scope' => 'snsapi_userinfo', # snsapi_userinfo （弹出授权页面，可通过openid拿到昵称、性别、所在地。并且，即使在未关注的情况下，只要用户授权，也能获取其信息）
                'state' => $state, # 重定向后会带上state参数，开发者可以填写a-zA-Z0-9的参数值，最多128字节
            ]) . '#wechat_redirect';
    }

    /**
     * Web授权回调中接受code,获取access_token
     * @param string $code code作为换取access_token的票据，每次用户授权带上的code将不一样，code只能使用一次，5分钟未被使用自动过期
     * @_param string $state
     * @return WebAuth
     * @throws WechatException
     */
    public function getWebAuthAccessToken(string $code): WebAuth
    {
        $url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={$this->appid}&secret={$this->appsecret}&code={$code}&grant_type=authorization_code";
        $data = null;
        try {
            $data = Network::getJson($url);
            WechatException::test($data);
            return new WebAuth($data);
        } catch (NetworkException $exception) {
            Logger::getLogger('addon.wechat')->fatal([
                'network:' . $exception->getMessage(), $data
            ]);
        } catch (WechatException $exception) {
            Logger::getLogger('addon.wechat')->fatal([
                'wechat:' . $exception->getMessage(), $data
            ]);
        }
        throw new WechatException('获取网页微信授权失败');
    }

    /**
     * 获取网页授权的微信信息
     * @param string $access_token
     * @param string $openid
     * @return Member
     * @throws WechatException
     */
    public function getWebAuthUserInfo(string $access_token, string $openid): Member
    {
        $url = "https://api.weixin.qq.com/sns/userinfo?access_token={$access_token}&openid={$openid}&lang=zh_CN";
        try {
            $data = Network::getJson($url);
            WechatException::test($data);
            return new Member($data);
        } catch (NetworkException $exception) {
            Logger::getLogger('addon.wechat')->fatal('network:' . $exception->getMessage());
        } catch (WechatException $exception) {
            Logger::getLogger('addon.wechat')->fatal('wechat:' . $exception->getMessage());
        }
        throw new WechatException('获取网页微信信息失败');
    }


    /**
     * 微信api不支持中文转义的json结构
     * @param array $arr
     * @return string
     */
    public static function jsonEncode(array $arr): string
    {
        if (count($arr) == 0) return "[]";
        $parts = [];
        $is_list = false;
        //Find out if the given array is a numerical array
        $keys = array_keys($arr);
        $max_length = count($arr) - 1;
        if (($keys [0] === 0) && ($keys [$max_length] === $max_length)) { //See if the first key is 0 and last key is length - 1
            $is_list = true;
            for ($i = 0; $i < count($keys); $i++) { //See if each key correspondes to its position
                if ($i != $keys [$i]) { //A key fails at position check.
                    $is_list = false; //It is an associative array.
                    break;
                }
            }
        }
        foreach ($arr as $key => $value) {
            if (is_array($value)) { //Custom handling for arrays
                if ($is_list)
                    $parts [] = self::jsonEncode($value); /* :RECURSION: */
                else
                    $parts [] = '"' . $key . '":' . self::jsonEncode($value); /* :RECURSION: */
            } else {
                $str = '';
                if (!$is_list)
                    $str = '"' . $key . '":';
                //Custom handling for multiple data types
                if (!is_string($value) && is_numeric($value) && $value < 2000000000)
                    $str .= $value; //Numbers
                elseif ($value === false)
                    $str .= 'false'; //The booleans
                elseif ($value === true)
                    $str .= 'true';
                else
                    $str .= '"' . addslashes($value) . '"'; //All other things
                $parts [] = $str;
            }
        }
        $json = implode(',', $parts);
        if ($is_list)
            return '[' . $json . ']'; //Return numerical JSON
        return '{' . $json . '}'; //Return associative JSON
    }

    /**
     * 过滤特殊字符
     * @param string $string
     * @return string
     */
    public static function encode($string): string
    {
        if (!$string) return '';
        $tmpStr = json_encode((string)$string); //暴露出unicode
        $tmpStr = preg_replace_callback("/(\\\ue[0-9a-f]{3})/", function ($val) {
            return addslashes($val[0]??'');
        }, $tmpStr); //将emoji的unicode留下，其他不动
        return json_decode($tmpStr);
    }


//    public static function decode($string): string
//    {
//        return preg_replace("#\\\u([0-9a-f]+)#ie", function ($val) {
//            return iconv('UCS-2', 'UTF-8', pack('H4', $val[0]??''));
//        }, (string)$string);
//
//    }

    /**
     * 获取web oauth2授权得到的微信openid
     * @return string
     */
    public function getOauth2Openid(): string
    {
        $openid = Cookie::get('openid');
        if (!$openid or !($openid = Base64x::decode($openid, SECRET_KEY))) {
            try {
                if (empty($_GET['code'])) {
                    Response::redirect($this->getWebAuthUrl(/** @var string 重定向地址 */
                        WS_PUBLIC_URL . $_SERVER['REQUEST_URI']));
                }
                $auth = $this->getWebAuthAccessToken($_GET['code']);
                $member = $this->getWebAuthUserInfo($auth->getAccessToken(), $auth->getOpenid());
                $membermodel = new MemberModel($member);
                $openid = $membermodel->openid;
                Cookie::set('openid', Base64x::encode($openid, SECRET_KEY), 7 * WS_ONE_DAY);
            } catch (\Throwable $exception) {
                Logger::getLogger('wechat.webauth')->fatal([
                    $exception->getMessage(), get_class($exception)
                ]);
                Response::sendHttpStatus(500, 'wechat oauth2 failed');
            }
        }
        return $openid;
    }

    /**
     * 企业支付
     * @param string $openid
     * @param int $amount
     * @param string $desc 付款描述
     * @param string $name 接受者真实姓名,如果为空则不校验
     * @return bool
     */
    public function pay(string $openid, int $amount, string $desc, string $name = ''): bool
    {
        $enterprise = new Enterprise($this->mch_appid, $this->mch_appsecret, $this->mch_id, $this->mch_ip);
        if ($enterprise->pay($openid, $amount, $desc,
            $name ? Enterprise::FORCE_CHECK : Enterprise::NO_CHECK, $name)
        ) {
            return true;
        } else {
            return false;
        }
    }

}